﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using DSS_XNA.Engine.Error;
using Microsoft.Xna.Framework;
using System.IO;
using DSS_XNA.Engine.Log;
using Microsoft.Xna.Framework.Input;
using Game.Engine.World.Worlds.RPGWorld;

namespace DSS_XNA.Engine.World.Worlds.RPGWorld
{
    public class TileMap
    {
        #region Properties

        public const int EmptyTileIndex = -1;

        public List<TileAnimation> Tiles
        {
            get { return _tiles; }
            set { _tiles = value; }
        }
        private List<TileAnimation> _tiles = new List<TileAnimation>();

        public int[] IndexArray
        {
            get { return _indexArray; }
            set { _indexArray = value; }
        }
        private int[] _indexArray = null;

        public TileProperties[] TileProperties
        {
            get { return _tileProperties; }
            set { _tileProperties = value; }
        }
        private TileProperties[] _tileProperties = null;

        public int TileWidthInPixels
        {
            get { return _tileWidthInPixels; }
            set { _tileWidthInPixels = value; }
        }
        private int _tileWidthInPixels = 0;

        public int TileHeightInPixels
        {
            get { return _tileHeightInPixels; }
            set { _tileHeightInPixels = value; }
        }
        private int _tileHeightInPixels = 0;

        public float TileWidthInMeters
        {
            get
            {
                return this.TileWidthInPixels / World.Meter;
            }
        }

        public float TileHeightInMeters
        {
            get
            {
                return this.TileHeightInPixels / World.Meter;
            }
        }

        public float DesiredTileWidthInMeters
        {
            get { return _desiredTileWidthInMeters; }
            set { _desiredTileWidthInMeters = value; }
        }
        private float _desiredTileWidthInMeters = 0.5f;

        public float DesiredTileHeightInMeters
        {
            get { return _desiredTileHeightInMeters; }
            set { _desiredTileHeightInMeters = value; }
        }
        private float _desiredTileHeightInMeters = 0.5f;

        public int MapWidthInTiles
        {
            get { return _mapWidthInTiles; }
            set { _mapWidthInTiles = value; }
        }
        private int _mapWidthInTiles = 0;

        public int MapHeightInTiles
        {
            get { return _mapHeightInTiles; }
            set { _mapHeightInTiles = value; }
        }
        private int _mapHeightInTiles = 0;

        // cannot modify an accessor directly when it is an object it seems...
        public Point Offset = new Point();

        public Game1 Engine
        {
            get { return _engine; }
            set { _engine = value; }
        }
        private Game1 _engine = null;

        #endregion

        #region Public Methods

        public TileMap()
        {
        }

        public void Create(int widthInTiles, int heightInTiles, int tileWidthInPixels,
            int tileHeightInPixels)
        {
            this.Destroy();

            this.MapWidthInTiles = widthInTiles;
            this.MapHeightInTiles = heightInTiles;
            this.TileWidthInPixels = tileHeightInPixels;
            this.TileHeightInPixels = tileWidthInPixels;

            this.IndexArray = new int[widthInTiles * heightInTiles];
            this.TileProperties = new TileProperties[widthInTiles * heightInTiles];
            for (int i = 0; i < widthInTiles * heightInTiles; i++)
                this.TileProperties[i] = new TileProperties();

            for (int i = 0; i < this.IndexArray.Length; i++)
                this.IndexArray[i] = EmptyTileIndex;
        }

        private void Destroy()
        {
            this.IndexArray = null;
            this.TileProperties = null;
            this.Tiles.Clear();
            this.Offset = new Point(0, 0);
        }

        public bool Render(SpriteBatch spriteBatch, TileSetManager tileSetManager)
        {
            int destWidth = (int)(World.Meter * this.DesiredTileWidthInMeters);
            int destHeight = (int)(World.Meter * this.DesiredTileHeightInMeters);

            Rectangle destRect = new Rectangle();
            destRect.X = 0;
            destRect.Y = 0;
            destRect.Width = destWidth;
            destRect.Height = destHeight;

            for (int x = 0; x < this.MapWidthInTiles; x++)
            {
                for (int y = 0; y < this.MapHeightInTiles; y++)
                {
                    int idxArrayPosition = x + (y * this.MapWidthInTiles);
                    int idx = this.IndexArray[idxArrayPosition];

                    destRect.X = x * destWidth;
                    destRect.Y = y * destHeight;

                    destRect.X += (int)this.Offset.X;
                    destRect.Y += (int)this.Offset.Y;

                    if (idx == EmptyTileIndex)
                        continue;

                    if (idx >= this.Tiles.Count)
                        throw new DSSException(String.Format("Index '{0}' is too great for tiles of count: {1} at x: {2} y: {3}",
                            idx, this.Tiles.Count, x, y));
                    if (idx < 0)
                        throw new DSSException(String.Format("Index '{0}' is less than 0 at x: {1} y: {2}", idx, x, y));

                    TileProperties properties = this.TileProperties[idxArrayPosition];
                    this.Tiles[idx].Render(spriteBatch, destRect, tileSetManager, properties);
                }
            }

            return true;
        }

        public void Update(GameTime gameTime, KeyboardState ks, MouseState ms, GamePadState gs)
        {
        }

        public void Save(string fileName, TileSetManager tileSetManager)
        {
            if (fileName == null)
                throw new DSSException("'fileName' is null.");
            if (fileName.Length == 0)
                throw new DSSException("'fileName' is zero-length.");
            if (tileSetManager == null)
                throw new DSSException("'tileSetManager' is null.");

            BinaryWriter writer = null;

            try
            {
                FileStream fp = File.Open(fileName, FileMode.Create, FileAccess.ReadWrite);
                writer = new BinaryWriter(fp);

                // FORMAT:
                // map width
                // map height
                // <tile width (pixels)>
                // <tile height (pixels)>
                // <desired tile width (meters)>
                // <desired tile height (meters)>
                // <num tile sets>
                // <tile sets...>
                //      <tile set filename (not the path, just the filename - the tileset is assumed to be in the tileset directory)>
                // -- foreach tile set, starting with the first in the <num tile sets> list --
                // <num tiles>
                // <tiles...>
                //      <source x,y> // no order because the tiles are printed in order of the tile sets above
                // <map with and height>
                //      <num anims>[<tile set idx>:<tile index in tiles list above>]
                // <tile properties>

                writer.Write(this.MapWidthInTiles);
                writer.Write(this.MapHeightInTiles);
                writer.Write(this.TileWidthInPixels);
                writer.Write(this.TileHeightInPixels);
                writer.Write((double)this.DesiredTileWidthInMeters);
                writer.Write((double)this.DesiredTileHeightInMeters);

                writer.Write(tileSetManager.TileSets.Count);
                foreach (TileSet ts in tileSetManager.TileSets)
                {
                    writer.Write(ts.FileName);
                }

                Dictionary<int, Dictionary<Microsoft.Xna.Framework.Rectangle, Tile>> tilesByTileSet = new Dictionary<int, Dictionary<Microsoft.Xna.Framework.Rectangle, Tile>>();
                foreach (TileAnimation anim in this.Tiles)
                {
                    foreach (Tile t in anim.Tiles)
                    {
                        if (!tilesByTileSet.ContainsKey(t.TileSetIdx))
                            tilesByTileSet.Add(t.TileSetIdx, new Dictionary<Rectangle, Tile>());

                        if (!tilesByTileSet[t.TileSetIdx].ContainsKey(t.SourceRect))
                            tilesByTileSet[t.TileSetIdx].Add(t.SourceRect, t);
                    }
                }

                foreach (TileSet ts in tileSetManager.TileSets)
                {
                    int idx = tileSetManager.TileSets.IndexOf(ts);
                    int numTiles = tilesByTileSet[idx].Count;

                    writer.Write(numTiles);

                    foreach (Tile tile in tilesByTileSet[idx].Values)
                    {
                        writer.Write(tile.SourceRect.X);
                        writer.Write(tile.SourceRect.Y);
                    }
                }

                foreach (int idx in this.IndexArray)
                {
                    // if there is no tile here...
                    if (idx == EmptyTileIndex)
                    {
                        writer.Write(EmptyTileIndex);
                        continue;
                    }

                    TileAnimation anim = this.Tiles[idx];

                    writer.Write(anim.Tiles.Count);
                    foreach (Tile t in anim.Tiles)
                    {
                        writer.Write(t.TileSetIdx);

                        // get the index of the tile in the list above
                        int tileIndexInListAbove = tilesByTileSet[t.TileSetIdx].Keys.ToList().IndexOf(t.SourceRect);
                        writer.Write(tileIndexInListAbove);
                    }
                }

                foreach (TileProperties p in this.TileProperties)
                {
                    writer.Write(p.IsCollidable);
                }
            }
            catch (Exception ex)
            {
                Logger.Log(String.Format("Failed to save world file '{0}'\n\n{1}",
                    fileName, ex), LogLevel.Major);
            }
            finally
            {
                if (writer != null)
                {
                    writer.Close();
                    writer = null;
                }
            }
        }

        public bool Load(string fileName, TileSetManager tileSetManager, Game1 engine)
        {
            if (fileName == null)
                throw new DSSException("'fileName' is null.");
            if (fileName.Length == 0)
                throw new DSSException("'fileName' is zero-length.");
            if (tileSetManager == null)
                throw new DSSException("'tileSetManager' is null.");
            if (engine == null)
                throw new DSSException("'engine' is null.");

            BinaryReader reader = null;

            this.Engine = engine;

            try
            {
                FileStream fp = File.Open(fileName, FileMode.Open, FileAccess.ReadWrite);
                reader = new BinaryReader(fp);

                this.MapWidthInTiles = reader.ReadInt32();
                this.MapHeightInTiles = reader.ReadInt32();
                this.TileWidthInPixels = reader.ReadInt32();
                this.TileHeightInPixels = reader.ReadInt32();
                this.DesiredTileWidthInMeters = (float)reader.ReadDouble();
                this.DesiredTileHeightInMeters = (float)reader.ReadDouble();

                int numTileSets = reader.ReadInt32();
                Dictionary<int, List<Tile>> tiles = new Dictionary<int, List<Tile>>();

                for (int i = 0; i < numTileSets; i++)
                {
                    string tileSetName = reader.ReadString();

                    tileSetManager.AddTileSet(tileSetName, engine);
                }

                for (int i = 0; i < numTileSets; i++)
                {
                    int numTiles = reader.ReadInt32();

                    for (int tidx = 0; tidx < numTiles; tidx++)
                    {
                        Rectangle sourceRect = new Rectangle();
                        sourceRect.X = reader.ReadInt32();
                        sourceRect.Y = reader.ReadInt32();
                        sourceRect.Width = this.TileWidthInPixels;
                        sourceRect.Height = this.TileHeightInPixels;

                        Tile tile = new Tile();
                        tile.TileSetIdx = i;
                        tile.SourceRect = sourceRect;

                        if (!tiles.ContainsKey(i))
                            tiles.Add(i, new List<Tile>());

                        tiles[i].Add(tile);
                    }
                }

                int totalTiles = this.MapWidthInTiles * this.MapHeightInTiles;
                this.IndexArray = new int[totalTiles];
                this.TileProperties = new TileProperties[totalTiles];
                for (int i = 0; i < totalTiles; i++)
                    this.TileProperties[i] = new TileProperties();

                for (int i = 0; i < totalTiles; i++)
                {
                    int numAnims = reader.ReadInt32();
                    if (numAnims == EmptyTileIndex)
                    {
                        this.IndexArray[i] = EmptyTileIndex;
                        continue;
                    }

                    TileAnimation anim = new TileAnimation();

                    for (int a = 0; a < numAnims; a++)
                    {
                        int tileSetIdx = reader.ReadInt32();
                        int tileIndex = reader.ReadInt32();

                        Tile tile = tiles[tileSetIdx][tileIndex];

                        anim.Tiles.Add(tile);
                    }

                    this.Tiles.Add(anim);
                    this.IndexArray[i] = this.Tiles.Count - 1;
                }

                for (int i = 0; i < totalTiles; i++)
                {
                    bool isCollidable = reader.ReadBoolean();

                    this.TileProperties[i].IsCollidable = isCollidable;
                }
            }
            catch (Exception ex)
            {
                Logger.Log(String.Format("Failed to load tile map '{0}'\n\n{1}",
                    fileName, ex), LogLevel.Major);
                return false;
            }
            finally
            {
                if (reader != null)
                {
                    reader.Close();
                    reader = null;
                }
            }

            return true;
        }

        public void FocusOnPoint(Point point)
        {
            if (point == null)
                throw new DSSException("'point' is null.");

            Point screenCenter = this.Engine.GraphicsDevice.ScissorRectangle.Center;

            this.Offset.X = -point.X + screenCenter.X;
            this.Offset.Y = -point.Y + screenCenter.Y;
        }

        #endregion
    }
}
